#include <stdint.h>
#include "redintobject.h"
#include "redfileobject.h"
#include "rednullobject.h"
#include "redtupleobject.h"
#include "redstringobject.h"
#include "redexceptionobject.h"

#define READ_CHUNK_SIZE		4096

static RedObject *file_len(RedObject *self)
{
	/* save current file pos */
	long pos = ftell(RedFile_FP(self));

	if (pos < 0)
	{
		clearerr(RedFile_FP(self));
		RedException_ThrowErrno(&RedExc_IOError);
		return NULL;
	}

	/* seek to the end */
	if (fseek(RedFile_FP(self), 0, SEEK_END) <= 0)
	{
		clearerr(RedFile_FP(self));
		RedException_ThrowErrno(&RedExc_IOError);
		return NULL;
	}

	/* the current file pos is file size */
	long size = ftell(RedFile_FP(self));

	if (size < 0)
	{
		clearerr(RedFile_FP(self));
		RedException_ThrowErrno(&RedExc_IOError);
		return NULL;
	}

	/* restore file pos */
	if (fseek(RedFile_FP(self), pos, SEEK_SET) <= 0)
	{
		clearerr(RedFile_FP(self));
		RedException_ThrowErrno(&RedExc_IOError);
		return NULL;
	}

	/* wrap with RedObject */
	return RedInt_FromInt(size);
}

static void file_clear(RedObject *self)
{
	Red_UNREF(RedFile_Mode(self));
	Red_UNREF(RedFile_Name(self));
	Red_UNREF(RedFile_Buffer(self));
}

static void file_release(RedObject *self)
{
	if (!RedFile_Std(self))
		if (RedFile_FP(self))
			fclose(RedFile_FP(self));

	file_clear(self);
	Red_RELEASE();
}

static void file_traverse(RedObject *self, tp_visit_fn_t visit, void *args)
{
	Red_VISIT(RedFile_Mode(self));
	Red_VISIT(RedFile_Name(self));
	Red_VISIT(RedFile_Buffer(self));
}

static RedObject *file_delattr(RedObject *self, RedObject *attr)
{
	RedException_ThrowFormat(&RedExc_AttributeError, "Cannot '__delattr__' for 'file' objects");
	return NULL;
}

static RedObject *file_getattr(RedObject *self, RedObject *attr)
{
		 if (RedString_EqualsCString(attr, "fd"))	return RedInt_FromInt(fileno(RedFile_FP(self)));
	else if (RedString_EqualsCString(attr, "mode"))	return Red_REF(RedFile_Mode(self)), RedFile_Mode(self);
	else if (RedString_EqualsCString(attr, "name"))	return Red_REF(RedFile_Name(self)), RedFile_Name(self);
	else											return self->ob_type->tp_parent->tp_getattr(self, attr);
}

static RedObject *file_setattr(RedObject *self, RedObject *attr, RedObject *value)
{
	RedException_ThrowFormat(&RedExc_AttributeError, "Cannot '__delattr__' for 'file' objects");
	return NULL;
}

static int extend_buffer(RedObject *self)
{
	FILE *fp = RedFile_FP(self);
	RedObject *prev = RedFile_Buffer(self);
	RedObject *chunk = RedString_FromLength(READ_CHUNK_SIZE);

	size_t size = fread(RedString_Buffer(chunk), 1, READ_CHUNK_SIZE, fp);

	/* adjust buffer size after read */
	RedString_Length(chunk) = (int)size;

	/* check for errors */
	if (ferror(fp))
	{
		clearerr(fp);
		Red_UNREF(chunk);
		RedException_ThrowErrno(&RedExc_IOError);
		return -1;
	}

	RedFile_Buffer(self) = RedString_Append(prev, chunk);
	Red_UNREF(prev);
	Red_UNREF(chunk);
	return (int)size;
}

static RedObject *file_read(RedObject *self, RedObject *args)
{
	if (!RedFile_Exact(self))
	{
		RedException_ThrowFormat(&RedExc_TypeError, "'file' expected for 'self' argument");
		return NULL;
	}

	int64_t length = -1;
	return RedTuple_Unpack(args, "i*", &length) ? RedFile_Read(self, (int)length) : NULL;
}

static RedObject *file_readall(RedObject *self, RedObject *args)
{
	if (RedFile_Exact(self))
		return RedTuple_Unpack(args, "") ? RedFile_ReadAll(self) : NULL;

	RedException_ThrowFormat(&RedExc_TypeError, "'file' expected for 'self' argument");
	return NULL;
}

static RedObject *file_readline(RedObject *self, RedObject *args)
{
	if (RedFile_Exact(self))
		return RedTuple_Unpack(args, "") ? RedFile_ReadLine(self) : NULL;

	RedException_ThrowFormat(&RedExc_TypeError, "'file' expected for 'self' argument");
	return NULL;
}

static RedObject *file_write(RedObject *self, RedObject *args)
{
	if (!RedFile_Exact(self))
	{
		RedException_ThrowFormat(&RedExc_TypeError, "'file' expected for 'self' argument");
		return NULL;
	}

	int len = -1;
	char *data = NULL;

	if (!RedTuple_Unpack(args, "s@", &len, &data))
		return NULL;

	if ((len = RedFile_WriteBuffer(self, data, len)) < 0)
		return NULL;

	return RedInt_FromInt(len);
}

static RedObject *file_tell(RedObject *self, RedObject *args)
{
	if (!RedFile_Exact(self))
	{
		RedException_ThrowFormat(&RedExc_TypeError, "'file' expected for 'self' argument");
		return NULL;
	}

	if (!RedTuple_Unpack(args, ""))
		return NULL;

	int pos = RedFile_Tell(self);
	return pos < 0 ? NULL : RedInt_FromInt(pos);
}

static RedObject *file_seek(RedObject *self, RedObject *args)
{
	if (!RedFile_Exact(self))
	{
		RedException_ThrowFormat(&RedExc_TypeError, "'file' expected for 'self' argument");
		return NULL;
	}

	int64_t pos = -1;
	int64_t whence = -1;

	if (!RedTuple_Unpack(args, "i*i*", &pos, &whence))
		return NULL;

	if (!RedFile_Seek(self, (int)pos, (int)whence))
		return NULL;

	Red_Return_Null();
}

static RedObject *file_flush(RedObject *self, RedObject *args)
{
	if (!RedFile_Exact(self))
	{
		RedException_ThrowFormat(&RedExc_TypeError, "'file' expected for 'self' argument");
		return NULL;
	}

	if (!RedTuple_Unpack(args, ""))
		return NULL;

	if (!RedFile_Flush(self))
		return NULL;

	Red_Return_Null();
}

static RedObject *file_close(RedObject *self, RedObject *args)
{
	if (!RedFile_Exact(self))
	{
		RedException_ThrowFormat(&RedExc_TypeError, "'file' expected for 'self' argument");
		return NULL;
	}

	if (!RedTuple_Unpack(args, ""))
		return NULL;

	if (!RedFile_FP(self))
	{
		RedException_ThrowFormat(&RedExc_IOError, "File already closed");
		return NULL;
	}

	if (!RedFile_Close(self))
		return NULL;

	Red_Return_Null();
}

static RedMethodDesc file_methods[] =
{
	{ "read"		, file_read			},
	{ "readall"		, file_readall		},
	{ "readline"	, file_readline		},
	{ "write"		, file_write		},
	{ "tell"		, file_tell			},
	{ "seek"		, file_seek			},
	{ "flush"		, file_flush		},
	{ "close"		, file_close		},
	{ NULL, NULL },
};

RedTypeObject RedType_File =
{
	RedType_INIT("file", RedFileObject, RedType_Object)

	.tp_len			= file_len,
	.tp_clear		= file_clear,
	.tp_release		= file_release,
	.tp_traverse	= file_traverse,

	.tp_delattr		= file_delattr,
	.tp_getattr		= file_getattr,
	.tp_setattr		= file_setattr,

	.tp_methods		= file_methods,
};

RedObject *RedFile_Wrap(const char *name, FILE *fp)
{
	RedObject *result = RedObject_Create(&RedType_File, NULL);

	if (!result)
		return NULL;

	RedFile_FP(result) = fp;
	RedFile_Std(result) = 1;
	RedFile_Mode(result) = RedString_FromCString("#");
	RedFile_Name(result) = RedString_FromCFormat("%s", name);
	RedFile_Buffer(result) = RedString_FromLength(0);

	/* the Red_UNREF will set result to NULL */
	if (!RedFile_Mode(result) || !RedFile_Name(result))
		Red_UNREF(result);

	return result;
}

RedObject *RedFile_Open(const char *name, const char *mode)
{
	FILE *fp = fopen(name, mode);

	if (!fp)
	{
		RedException_ThrowErrno(&RedExc_IOError);
		return NULL;
	}

	RedObject *result = RedObject_Create(&RedType_File, NULL);

	if (!result)
	{
		fclose(fp);
		return NULL;
	}

	RedFile_FP(result) = fp;
	RedFile_Std(result) = 0;
	RedFile_Mode(result) = RedString_FromCFormat("%s", mode);
	RedFile_Name(result) = RedString_FromCFormat("%s", name);
	RedFile_Buffer(result) = RedString_FromLength(0);

	/* the Red_UNREF will set result to NULL */
	if (!RedFile_Mode(result) || !RedFile_Name(result) || !RedFile_Buffer(result))
		Red_UNREF(result);

	return result;
}

RedObject *RedFile_Read(RedObject *self, int size)
{
	if (!RedFile_FP(self))
	{
		RedException_ThrowFormat(&RedExc_IOError, "File already closed");
		return NULL;
	}

	if ((size > RedString_Length(RedFile_Buffer(self))) && (extend_buffer(self) < 0))
		return NULL;

	/* do NOT use variable `buffer` here since it may replaced above */
	RedObject *result = RedString_FromBufferAndSize(RedString_Buffer(RedFile_Buffer(self)), size);

	RedString_Eat(RedFile_Buffer(self), size);
	return result;
}

RedObject *RedFile_ReadAll(RedObject *self)
{
	if (!RedFile_FP(self))
	{
		RedException_ThrowFormat(&RedExc_IOError, "File already closed");
		return NULL;
	}

	int size;
	while ((size = extend_buffer(self)) > 0);

	if (size < 0)
		return NULL;

	RedObject *result = RedFile_Buffer(self);

	RedFile_Buffer(self) = RedString_FromLength(0);
	return result;
}

RedObject *RedFile_ReadLine(RedObject *self)
{
	if (!RedFile_FP(self))
	{
		RedException_ThrowFormat(&RedExc_IOError, "File already closed");
		return NULL;
	}

	int i = 0;
	int n = RedString_Length(RedFile_Buffer(self));
	char *s = RedString_Buffer(RedFile_Buffer(self));

	for (;;)
	{
		/* find '\r' or '\n' */
		while (i < n && s[i] != '\r' && s[i] != '\n')
			i++;

		if (i < n)
		{
			if (s[i] == '\n')
			{
				i++;					/* '\n' */
			}
			else if (s[i] == '\r')
			{
				if (s[i + 1] != '\n')
					i++;				/* '\r' */
				else
					i += 2;				/* '\r\n' */
			}

			/* found, dump out and eat */
			RedObject *result = RedString_FromBufferAndSize(s, i);

			RedString_Eat(RedFile_Buffer(self), i);
			return result;
		}

		/* end of buffer, extend it */
		int x = extend_buffer(self);

		/* IO error when extending */
		if (x < 0)
			return NULL;

		/* file drained, just return all remains */
		if (x == 0)
		{
			RedObject *result = RedFile_Buffer(self);

			RedFile_Buffer(self) = RedString_FromLength(0);
			return result;
		}

		n = RedString_Length(RedFile_Buffer(self));
		s = RedString_Buffer(RedFile_Buffer(self));
		continue;
	}
}

int RedFile_Write(RedObject *self, RedObject *data)
{
	if (RedString_Exact(data))
		return RedFile_WriteBuffer(self, RedString_Buffer(data), RedString_Length(data));

	RedException_ThrowFormat(&RedExc_TypeError, "file.write() only accepts strings");
	return -1;
}

int RedFile_WriteBuffer(RedObject *self, const char *data, int size)
{
	if (!RedFile_FP(self))
	{
		RedException_ThrowFormat(&RedExc_IOError, "File already closed");
		return -1;
	}

	size = (int)fwrite(data, 1, size, RedFile_FP(self));

	if (!ferror(RedFile_FP(self)))
		return size;

	clearerr(RedFile_FP(self));
	RedException_ThrowErrno(&RedExc_IOError);
	return -1;
}

int RedFile_Tell(RedObject *self)
{
	if (!RedFile_FP(self))
	{
		RedException_ThrowFormat(&RedExc_IOError, "File already closed");
		return -1;
	}

	long pos = ftell(RedFile_FP(self));

	if (pos >= 0)
		return (int)pos;

	clearerr(RedFile_FP(self));
	RedException_ThrowErrno(&RedExc_IOError);
	return -1;
}

char RedFile_Seek(RedObject *self, int pos, int whence)
{
	if (!RedFile_FP(self))
	{
		RedException_ThrowFormat(&RedExc_IOError, "File already closed");
		return 0;
	}

	if (fseek(RedFile_FP(self), (long)pos, (int)whence) >= 0)
		return 1;

	clearerr(RedFile_FP(self));
	RedException_ThrowErrno(&RedExc_IOError);
	return 0;
}

char RedFile_Flush(RedObject *self)
{
	if (!RedFile_FP(self))
	{
		RedException_ThrowFormat(&RedExc_IOError, "File already closed");
		return 0;
	}

	if (fflush(RedFile_FP(self)) >= 0)
		return 1;

	clearerr(RedFile_FP(self));
	RedException_ThrowErrno(&RedExc_IOError);
	return 0;
}

char RedFile_Close(RedObject *self)
{
	if (fclose(RedFile_FP(self)) >= 0)
	{
		RedFile_FP(self) = NULL;
		return 1;
	}

	RedFile_FP(self) = NULL;
	RedException_ThrowErrno(&RedExc_IOError);
	return 0;
}
